Skip to content

fix: config UX & security validation (PR2 of bug-audit-v2)#109

Merged
lesnik512 merged 8 commits into
mainfrom
fix/bug-audit-v2-pr2-config-security
Jun 5, 2026
Merged

fix: config UX & security validation (PR2 of bug-audit-v2)#109
lesnik512 merged 8 commits into
mainfrom
fix/bug-audit-v2-pr2-config-security

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

PR2 of the 2026-06-05 bug-audit-v2 plan. Closes six config-layer findings — three UX rough edges (FastAPI app stomping, missing FastStream config fields) and three security validators (root_path escape, insecure OTel exporter, unsafe CORS). All validation happens at __post_init__ time on frozen dataclass configs so the entire instrument lifecycle is unreached on misconfiguration. Sequenced per planning/specs/2026-06-05-bug-audit-v2-sequencing.md; per-task TDD plan at planning/plans/2026-06-05-pr2-config-security.md.

Findings closed

  • UX-1FastAPIConfig.__post_init__ no longer stomps application.title/debug/version when the user supplies a pre-configured FastAPI() instance. The three assignments moved inside the UnsetType branch so they only apply when lite-bootstrap built the app.
  • UX-2 — Added prometheus_collector_registry: CollectorRegistry | None = None to FastStreamConfig. When non-None, FastStreamPrometheusInstrument uses the injected registry (via __post_init__); otherwise the existing per-instance default is preserved. Backward-compatible.
  • UX-3 — Added opentelemetry_excluded_urls: list[str] to FastStreamConfig, matching the existing fields on FastAPIConfig/LitestarConfig. _build_excluded_urls already read via getattr fallback so behavior is unchanged; the field is now discoverable.
  • SEC-1enable_offline_docs validates request.scope["root_path"] through the project's existing is_valid_path allowlist before interpolating into Swagger/Redoc HTML. Invalid paths fall back to empty with a warning. Covers the X-Forwarded-Prefix proxy-header injection vector.
  • SEC-2 — Added OpenTelemetryConfig.__post_init__ that emits a warning when opentelemetry_endpoint resolves to a non-local host AND opentelemetry_insecure=True (the unfortunate default). localhost, 127.0.0.1, ::1, and unix:// endpoints are silent. Host parsing handles both host:port (canonical OTLP gRPC) and scheme://host:port, including IPv6 brackets.
  • SEC-3 — Added CorsConfig.__post_init__ that raises ConfigurationError when cors_allowed_credentials=True is combined with a wildcard origin ("*" in the list) or a permissive regex (.*, .+). FastAPI's CORSMiddleware silently refuses the combo; Litestar's behavior differs. Validation at config-construction is consistent across frameworks.

Implementation notes

  • Two reviewer-driven course corrections during execution:

    • SEC-2's host parser initially used urllib.parse.urlparse + path.split(":")[0] fallback. Verified that this misparses the canonical localhost:4317 OTLP form (urlparse treats localhost as the scheme and yields path='4317'). Fixed by prepending // to schemeless inputs so urlparse always returns a correct hostname. Added parametrized regression tests covering five local forms (localhost:4317, 127.0.0.1:4317, [::1]:4317, http://localhost:4317, https://127.0.0.1:4317) and three remote forms (collector.example.com:4317, http://collector.example.com:4317, grpc://collector.example.com:4317).
    • SEC-3's first landing broke SEC-2's cascade for FastAPIConfig users. CorsConfig precedes OpenTelemetryConfig in FastAPIConfig's MRO; the new CorsConfig.__post_init__ returned early on the default (no credentials) without calling super().__post_init__(), so the OTel warning never fired for FastAPIConfig. Fixed by establishing the cascade invariant: BaseConfig gains a no-op __post_init__ as the terminator; every config-class __post_init__ calls super().__post_init__() after its own logic.
  • One subtle Python quirk surfaced: super(FastAPIConfig, self).__post_init__() is required in FastAPIConfig.__post_init__ rather than bare super(). @dataclass(slots=True) replaces the class object after the class body compiles, breaking the compile-time __class__ cell that bare super() relies on. Comment in the code documents the constraint.

Non-blocking follow-ups flagged by the final reviewer

  • Parametrize the cascade test across LitestarConfig, FreeConfig, FastStreamConfig for explicit regression coverage of the other framework configs (currently covered transitively via the shared OpenTelemetryConfig.__post_init__, but not directly).
  • The OTel warning's stacklevel=2 now lands on lite-bootstrap internals (due to the cascade) rather than user code. Acceptable — the warning text is actionable enough that users can find the offending config.

Test plan

  • just test — 175 passed (164 PR1 baseline ↔ unrelated to PR2; PR2 adds 11 new tests on its own pre-PR1 baseline), 100% coverage.
  • just lint-ci — clean (ruff, eof-fixer, ty).
  • No new warnings in pytest output (silenced the legacy "otl" test-fixture warning via a localhost:4317 swap in test_config.py).
  • Each commit is scoped to one finding ID so review/revert can land at the task level; plus one cross-cutting cascade-fix commit at the end.

Sequencing note

PR2 branches off main (NOT off PR1's branch). When PR1 (#108) merges first, PR2 rebases cleanly — they touch overlapping files (opentelemetry_instrument.py, fastapi_bootstrapper.py) but at non-overlapping line ranges. If PR2 merges first, PR1's diff narrows similarly.

🤖 Generated with Claude Code

@lesnik512 lesnik512 self-assigned this Jun 5, 2026
@lesnik512 lesnik512 force-pushed the fix/bug-audit-v2-pr2-config-security branch from ab30f94 to 108b59d Compare June 5, 2026 16:54
@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...te_bootstrap/bootstrappers/fastapi_bootstrapper.py 100.00% <100.00%> (ø)
...bootstrap/bootstrappers/faststream_bootstrapper.py 100.00% <100.00%> (ø)
lite_bootstrap/helpers/fastapi_helpers.py 100.00% <100.00%> (ø)
lite_bootstrap/instruments/base.py 100.00% <100.00%> (ø)
lite_bootstrap/instruments/cors_instrument.py 100.00% <100.00%> (ø)
..._bootstrap/instruments/opentelemetry_instrument.py 100.00% <100.00%> (ø)
tests/instruments/test_cors_instrument.py 100.00% <100.00%> (ø)
tests/instruments/test_opentelemetry_instrument.py 100.00% <100.00%> (ø)
tests/test_fastapi_bootstrap.py 100.00% <100.00%> (ø)
tests/test_fastapi_offline_docs.py 100.00% <100.00%> (ø)
... and 1 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

lesnik512 and others added 8 commits June 5, 2026 19:56
Per-task TDD steps covering UX-1, UX-2, UX-3, SEC-1, SEC-2, SEC-3 (plus folded
TEST-NEW-1 and TEST-NEW-6) from the 2026-06-05 audit. Task ordering accounts
for one cross-task interaction: Task 5 (SEC-2) adds super().__post_init__()
to FastAPIConfig.__post_init__ which Task 1 (UX-1) restructures first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the application.title/.debug/.version assignments inside the UnsetType
branch so they only apply when lite-bootstrap built the FastAPI() instance.
Pre-configured user apps now keep their construction-time values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…UX-2)

Add prometheus_collector_registry: CollectorRegistry | None config field on
FastStreamConfig. When non-None, FastStreamPrometheusInstrument uses the
injected registry; otherwise the existing per-instance default is preserved.
Lets users expose metrics that were registered on a shared registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FastAPIConfig and LitestarConfig already expose opentelemetry_excluded_urls;
FastStream relied on getattr fallback with no discoverable field. Add the field
to FastStreamConfig matching the FastAPI/Litestar pattern. _build_excluded_urls
behavior is unchanged — the getattr access still works the same way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…C-1)

The swagger/redoc handlers reflected request.scope["root_path"] verbatim into
HTML script/link tags. In default ASGI deployments root_path comes from server
config (uvicorn --root-path), but if a user enables ProxyHeadersMiddleware with
trusted X-Forwarded-Prefix, a malicious proxy could inject script content.
Validate via the project's existing is_valid_path allowlist and fall back to
empty (with a warning) on invalid input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add OpenTelemetryConfig.__post_init__ that emits a warning when
opentelemetry_endpoint points to a non-local host AND opentelemetry_insecure is
True (the unfortunate default). Localhost, 127.0.0.1, ::1, and unix:// endpoints
are silent. FastAPIConfig.__post_init__ now calls super().__post_init__() so
FastAPI users see the warning too; other framework configs (FreeConfig,
FastStreamConfig, LitestarConfig) don't have their own __post_init__ and
inherit the new behavior automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(SEC-3)

cors_allowed_credentials=True with cors_allowed_origins=["*"] (or a permissive
regex like ".*"/".+") is the canonical CORS misconfiguration. FastAPI's
CORSMiddleware refuses to set the credentials header in that combo but
Litestar's behavior differs and may silently accept it. Add a CorsConfig
__post_init__ that raises ConfigurationError on the unsafe combination
at config-construction time, before any framework sees the values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CorsConfig.__post_init__ (SEC-3) and OpenTelemetryConfig.__post_init__
(SEC-2) now call super().__post_init__() so the MRO chain propagates.
Before this, CorsConfig.__post_init__ blocked the cascade for FastAPIConfig
users (Cors precedes OpenTelemetry in MRO), so SEC-2's warning didn't fire.
BaseConfig gains a no-op __post_init__ as the chain terminator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 force-pushed the fix/bug-audit-v2-pr2-config-security branch from 108b59d to 895f2b1 Compare June 5, 2026 16:56
@lesnik512 lesnik512 merged commit c4f1d00 into main Jun 5, 2026
6 checks passed
@lesnik512 lesnik512 deleted the fix/bug-audit-v2-pr2-config-security branch June 5, 2026 16:58
lesnik512 added a commit that referenced this pull request Jun 5, 2026
Three PRs (#108, #109, #110), 26 findings closed, 153 → 187 tests, 100% coverage
throughout. Captures what went well (per-PR sequencing, two-stage review catching
three real bugs), what didn't (three implementer-hallucination incidents,
cross-task __post_init__ cascade missed in PR2 plan, OTel API misread in audit,
SEC-2 host parser untested against canonical localhost:4317 form), and six
action items to harden the pipeline for the next audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant